'''TestSuiteTreeCtrl

This is a tree control specifically for working with Robot test
suites. 

Each test in the tree will have a checkbox next to it, as well as
an icon. The icon can be changed to different colors depending on
state ("default", "run", "pass", "fail"). 

'''

import wx
from wx.lib.embeddedimage import PyEmbeddedImage
try:
    import wx.lib.agw.customtreectrl as customtreectrl
except ImportError:
    import wx.lib.customtreectrl as customtreectrl
from robot.parsing.model import TestData, TestCaseFile, TestDataDirectory, TestCase, UserKeyword
import rwb.robotfilter

RSAEvent, EVT_RSA = wx.lib.newevent.NewEvent()

class TreeNode:
    def __init__(self, longname, obj):
        self.longname= longname
        self.tcuk = obj
        self.obj = obj

    @property
    def name(self):
        return self.tcuk.name

FILTER_NONE = "filter_none"
FILTER_TAG  = "filter_tag"
FILTER_NAME = "filter_name"
    
ID_SELECT_ALL = wx.NewId()
ID_DESELECT_ALL = wx.NewId()
ID_SELECT_CHILDREN = wx.NewId()
ID_DESELECT_CHILDREN = wx.NewId()
ID_NEW_TESTCASE = wx.NewId()
ID_NEW_SUITE = wx.NewId()
ID_DELETE_ITEM=wx.NewId()
ID_RSA = wx.NewId()

wxEVT_TREE_BEGIN_LABEL_EDIT = wx.wxEVT_COMMAND_TREE_BEGIN_LABEL_EDIT

class TestSuiteTreeCtrl(customtreectrl.CustomTreeCtrl):
    '''A tree control designed to manage a Robot test suite'''
    if wx.VERSION <= (2,8,9):
        style=wx.SIMPLE_BORDER
        style |= wx.TR_HIDE_ROOT
        style |= wx.TR_HAS_BUTTONS
        style |= wx.TR_AUTO_CHECK_CHILD
        style |= wx.TR_HAS_VARIABLE_ROW_HEIGHT
    else:
        style = customtreectrl.TR_HIDE_ROOT
        style |= customtreectrl.TR_HAS_BUTTONS
        style |= customtreectrl.TR_AUTO_CHECK_CHILD
        style |= customtreectrl.TR_HAS_VARIABLE_ROW_HEIGHT

    def __init__(self, parent, id=wx.ID_ANY, size=(-1,-1)):
        try:
            # older versions of customtreectrl don't support agwStyle
            customtreectrl.CustomTreeCtrl.__init__(self, parent, id=id, 
                                                   size=size, 
                                                   style=wx.SIMPLE_BORDER,
                                                   agwStyle=self.style)
        except TypeError:
            customtreectrl.CustomTreeCtrl.__init__(self, parent, id=wx.ID_ANY, 
                                                   size=size,
                                                   style=self.style)

        self.SetFont(self.GetTopLevelParent().GetFont())
        self._initialize_images()
        # nodes lets us get to the node of a specific test by its
        # ID without having to traverse the whole tree
        self._nodes = {}
        self._tip = None
        self._suite = None

        self._test_case_popup = TestCasePopup()
        self._test_suite_dir_popup = TestSuiteDirectoryPopup()
        self._test_case_file_popup = TestCaseFilePopup()

        self.Bind(wx.EVT_CONTEXT_MENU, self.OnShowPopup)
        self.Bind(wx.EVT_TREE_END_LABEL_EDIT, self.OnEndLabelEdit)

        for popup in (self._test_case_popup, 
                      self._test_suite_dir_popup,
                      self._test_case_file_popup):

            wx.EVT_MENU(popup, ID_SELECT_ALL, self.OnSelectAll)
            wx.EVT_MENU(popup, ID_DESELECT_ALL, self.OnDeselectAll)
            wx.EVT_MENU(popup, ID_SELECT_CHILDREN, self.OnSelectChildren)
            wx.EVT_MENU(popup, ID_DESELECT_CHILDREN, self.OnDeselectChildren)
            wx.EVT_MENU(popup, ID_RSA, self.OnRSA)
            wx.EVT_MENU(popup, ID_DELETE_ITEM, self.OnDelete)
            wx.EVT_MENU(popup, ID_NEW_TESTCASE, self.OnNewTestCase)
            wx.EVT_MENU(popup, ID_NEW_SUITE, self.OnNewTestSuite)
        
    # copied from customtreectrl.py; added call to 
    # self._textCtrl.SelectAll()
    def Edit(self, item):
        """
        Internal function. Starts the editing of an item label, sending a
        ``EVT_TREE_BEGIN_LABEL_EDIT`` event.

        :param `item`: an instance of L{GenericTreeItem}.        
        """

        te = customtreectrl.TreeEvent(wxEVT_TREE_BEGIN_LABEL_EDIT, self.GetId())
        te._item = item
        te.SetEventObject(self)
        if self.GetEventHandler().ProcessEvent(te) and not te.IsAllowed():
            # vetoed by user
            return
    
        # We have to call this here because the label in
        # question might just have been added and no screen
        # update taken place.
        if self._dirty:
            if wx.Platform in ["__WXMSW__", "__WXMAC__"]:
                self.Update()
            else:
                wx.YieldIfNeeded()

        if self._textCtrl != None and item != self._textCtrl.item():
            self._textCtrl.StopEditing()

        self._textCtrl = customtreectrl.TreeTextCtrl(self, item=item)
        self._textCtrl.SetFocus()
        self._textCtrl.SelectAll()

    def OnEndLabelEdit(self, event):
        if event.IsEditCancelled():
            return None
        item = event.GetItem()
        new_name = event.GetLabel()
        # need to protect against duplicate names.
        # should we just automagically append #n,
        # or pop up an annoying message?
        pydata = self.GetItemPyData(item)
        tcuk = pydata.tcuk
        tcuk.name = new_name
        event.Skip()
        # FIXME: need to mark as dirty


    def _initialize_images(self):
        self._image_list = wx.ImageList(16,16)
        self._images = {
            "folder":  self._image_list.Add(wx.ArtProvider.GetBitmap(wx.ART_FOLDER, 
                                                                     wx.ART_OTHER, 
                                                                     (16,16))),
            "folder_open": self._image_list.Add(wx.ArtProvider.GetBitmap(wx.ART_FOLDER, 
                                                                         wx.ART_OTHER, 
                                                                         (16,16))),
            "default": self._image_list.Add(getWhiteBulletBitmap()),
            "running": self._image_list.Add(getBlueBulletBitmap()),
            "pass":    self._image_list.Add(getGreenBulletBitmap()),
            "fail":    self._image_list.Add(getRedBulletBitmap()),
            }
        self.SetImageList(self._image_list)

    def OnNewTestSuite(self, event):
        item = self.GetSelection()
        # if item is a file suite, create new suite in parent
        # if item is a directory suite, create new suite in item
        tcuk = self.GetItemPyData(item).tcuk
        raise Exception("not implemented yet")
        
    def OnNewTestCase(self, event):
        '''Create a new test case, update the tree, etc'''
        item = self.GetSelection()
        tcuk = self.GetItemPyData(item).tcuk
        if isinstance(tcuk, TestCase):
            suite = tcuk.parent.parent
        elif isinstance(tcuk, TestCaseFile):
            suite = tcuk
        else:
            raise Exception("unexpected object: %s" % str(tcuk))
        self.NewTestCase(suite)

    def NewTestCase(self, suite, name="New Test Case", showNotebookTab=True):
        '''Do the work of creating the test case'''
        existing_names = [testcase.name for testcase in suite.testcase_table.tests]
        new_name = name
        i = 1
        while new_name in existing_names:
            new_name = "%s #%s" % (name, i)
            i += 1
        new_testcase = suite.testcase_table.add(new_name)

        # FIXME: this needs to be placed in the proper
        # sort order...
        parent_node = self._nodes[suite]
        new_item = self._addTestCase(parent_node, new_testcase)
        self.EnsureVisible(new_item)
        wx.CallAfter(self.EditLabel, new_item)

    def OnDelete(self, event):
        item = self.GetSelection()
        tcuk = self.GetItemPyData(item).tcuk
        raise Exception("not implemented yet")
    
    def OnRSA(self, event):
        item = self.GetSelection()
        tcuk = self.GetItemPyData(item).tcuk
        new_event = RSAEvent(tcuk = tcuk)
        new_event.SetEventObject(self)
        wx.PostEvent(self, new_event)

    def Filter(self, filter_type, filter_string=""):
        # hmmm; this isn't the right place for this because
        # it only saves the state of nodes that are visible.
        # Need to rethink the logical flow a bit...
        saved_state = self.SaveState()

        self.DeleteAllItems()
        self._nodes = {}
        self._root = self.AddRoot("root")

        if filter_type == FILTER_TAG and filter_string != "":
            f = rwb.robotfilter.TestCaseTagFilter(self._suite)
            matching = f.filter(filter_string, rwb.robotfilter.STRING)
        elif filter_type == FILTER_NAME and filter_string != "":
            f = rwb.robotfilter.TestCaseNameFilter(self._suite)
            matching = f.filter(filter_string, rwb.robotfilter.STRING)
        elif filter_type == FILTER_NONE or filter_string == "":
            f = rwb.robotfilter.NullFilter(self._suite)
            matching = f.filter()

        for result in matching:
            self.AddNode(result)

        self.RestoreState(saved_state)

    def AddNode(self, obj):
        parent_node = self.GetParentNode(obj)
        node = self.AppendItem(parent_node,
                               obj.name,
                               image=self._images["default"])
        self.SetItemPyData(node, TreeNode(obj.name, obj))
        self._nodes[obj] = node
                               
                               
    def GetParentNode(self, obj):
        '''Create nodes for all ancestors of obj'''
        ancestors = rwb.robotfilter.GetAncestors(obj)
        node = self._root
        suite = None
        # search the ancestors starting at the most immediate ancestor
        for suite in reversed(ancestors):
            if suite in self._nodes:
                node = self._nodes[suite]
            else:
                node = self.AppendItem(node, 
                                       suite.name, 
                                       image=self._images["folder"])
                self._nodes[suite]  = node
                self.SetItemPyData(node, TreeNode(suite.name, suite))
                node.Expand()

        if isinstance(suite, TestCaseFile):
            # Danger Will Robinson! a user keyword reports True
            # for both isinstance(obj,UserKeyword) and 
            # isinstance(obj, TestCase). WTF? So, we need to
            # test for UserKeyword first since it's a subset of
            # a test case.
            if isinstance(obj, UserKeyword):
                if suite.keyword_table in self._nodes:
                    node = self._nodes[suite.keyword_table]
                else:
                    node = self.AppendItem(node, 
                                           "User Keywords",
                                           image=self._images["folder"]
                                           )
                    self.SetItemPyData(node, TreeNode("", suite.keyword_table))
                    self._nodes[suite.keyword_table] = node
            elif isinstance(obj, TestCase):
                if suite.testcase_table in self._nodes:
                    node = self._nodes[suite.testcase_table]
                else:
                    node = self.AppendItem(node, 
                                           "Test Cases",
                                           image=self._images["folder"]
                                           )
                    self.SetItemPyData(node, TreeNode("", suite.testcase_table))
                    self._nodes[suite.testcase_table] = node
        return node

    def GetTestCasesNode(self, suite):
        '''Return the node for the test cases of this suite

        Create the node if it doesn't exist
        '''
        suite_node = self._nodes[suite]
        testcases_node = None
        for child in suite_node.GetChildren():
            try:
                pydata = self.GetItemPyData(child)
            except Exception, e:
                continue

        return suite_node
        
    def SelectNodeByName(self, longname):
        '''This works, but only selects the item in the tree;
        it doesn't also show it in the view. Stupid wxPython doesn't
        fire the Activate event when I programatically set it. Feh.
        '''
        if longname in self._nodes:
            item = self._nodes[longname]
            self.SelectItem(item, True)

    def SelectOnlyFailedTests(self):
        for item in self._nodes.values():
            image = self.GetItemImage(item)
            self.CheckItem(item, image == self._images["fail"])

    def OnShowPopup(self, event):
        '''Show the appropriate popup menu for the selected item'''
        item = self.GetSelection()
        tcuk = self.GetItemPyData(item).tcuk

        if isinstance(tcuk, TestCaseFile):
            popup = self._test_case_file_popup
        elif isinstance(tcuk, TestDataDirectory):
            popup = self._test_suite_dir_popup
        elif isinstance(tcuk, TestCase):
            popup = self._test_case_popup
        else:
            popup = None
                            
        if popup is not None:
            popup.update(tree=self)
            pos = event.GetPosition()
            pos = self.ScreenToClient(pos)
            self.PopupMenu(popup, pos)

    def OnExpandAll(self, event):
        self.ExpandAll()

    def OnCollapseAll(self, event):
        '''Collapse all test suite files

        This collapses all test suite files, leaving test suite
        directories expanded. The net effect is you see a list
        of suites with no test cases. I *think* that makes more
        sense than collapsing everyting, leaving only the root 
        '''

        for item in self._nodes.values():
            tcuk = self.GetItemPyData(item).tcuk
            if isinstance(tcuk, TestCaseFile):
                self.Collapse(item)

    def OnSelectAll(self, event):
        '''Select all items with checkboxes'''
        for item in self._nodes.values():
            self.CheckItem(item, True)

    def OnDeselectAll(self, event):
        '''De-select all items with checkboxes'''
        for item in self._nodes.values():
            self.CheckItem(item, False)
            
    def OnSelectChildren(self, event):
        item = self.GetSelection()
        self.AutoCheckChild(item, True)

    def OnDeselectChildren(self, event):
        '''De-select all child items with checkboxes'''
        item = self.GetSelection()
        self.AutoCheckChild(item, False)

    def Reset(self):
        '''Reset the running/pass/fail state of all nodes'''
        for node in self._nodes.values():
            self.SetItemImage(node, self._images["default"])

    def GetNode(self, longname):
        item = self._nodes[longname]
        node = self.GetItemPyData(item)
        return  node

    def GetCheckedTests(self):
        result = []
        for (testId, item) in self._nodes.iteritems():
            if self.IsItemChecked(item):
                result.append(self.GetItemPyData(item).name)
#                result.append(self.GetItemPyData(item)[1])
        return result

    def GetCheckedTestsByName(self):
        '''Return a list of (suite name, test name) tuples for all checked tests'''
        result = []
        for (testId, item) in self._nodes.iteritems():
            if self.IsItemChecked(item):
#                item_name = self.GetItemPyData(item)[1].name
                item_name = self.GetItemPyData(item).name
                parent_item = self.GetItemParent(item)
#                parent_name = self.GetItemPyData(parent_item)[0]
                parent_name = self.GetItemPyData(parent_item).longname
                result.append((parent_name, item_name))

        return result

    def SetSuite(self, suite):
        self._suite = suite

    def AddSuite(self, suite):
        # add a suite to the root note
        self._suite = suite
        self.Filter(FILTER_NONE)
        
    def Redraw(self):
        self.Filter(FILTER_NONE)

    def SetState(self, testId, state):
        image = self._images[state]
        node = self._nodes[testId]
        self.SetItemImage(node, self._images[state])


    def _get_longname(self, obj):
        longname = []
        while obj is not None:
            # The darn robot object model doesn't have a simple
            # parent/child relationship between suites and test cases.
            # The parent of a test case is a test case table rather
            # than a test suite. Go figure. So, only keep track of the
            # names of suites and test cases and not the intermediate
            # objects
            if (isinstance(obj, TestDataDirectory) or 
                isinstance(obj, TestCaseFile) or 
                isinstance(obj, TestCase)):
                longname.append(obj.name)
            obj = obj.parent
        name = ".".join(reversed(longname))
        return name


    def _addKeyword(self, parent_node, kw):
        item = self.AppendItem(parent_node, kw.name)
        fullname = self._get_longname(kw)
        self.SetItemPyData(item, TreeNode(fullname, kw))
        self._nodes[kw] = item
        return item
        
    def _addTestCase(self, parent_node, testcase):
        '''Add a testcase to the tree'''
        item = self.AppendItem(parent_node, testcase.name, ct_type=1, image=self._images["default"])
        fullname = self._get_longname(testcase)
#        self.SetItemPyData(item, (fullname, test))
        self.SetItemPyData(item, TreeNode(fullname, testcase))
        self._nodes[testcase] = item
        return item

    def RestoreState(self, state):
        '''Restore the checked and expanded state of all known nodes in the tree'''
        for node in self._nodes.values():
            pydata = self.GetItemPyData(node)
            tcuk = pydata.tcuk
            # if we find a node for which there is no state information,
            # default to expanded, not checked.
            (checked, expanded) = state.get(tcuk, (False, True))
            self.CheckItem(node, checked)
            if expanded:
                self.Expand(node)
            else:
                self.Collapse(node)

    def SaveState(self):
        '''Return a dictionary of checked/expanded states for each node in the tree

        This was primarily designed for saving and restoring the state of
        the tree immediately before and after a refresh, respectively
        '''
        state = {}
        for node in self._nodes.values():
#            robject = self.GetItemPyData(node)[1]
            tcuk = self.GetItemPyData(node).tcuk
            checked = self.IsItemChecked(node)
            expanded = self.IsExpanded(node)
            state[tcuk] = (checked, expanded)
        return state


def get_ancestors(obj):
    '''Return a list of ancestor (test suite) objects for a test case or suite'''
    result = []
    parent = obj.parent
    while parent:
        # The robot object model doesn't have a simple parent/child
        # relationship between suites and test cases.  The parent of a
        # test case is a test case table rather than a test suite. 
        # This only keeps track of suites and testcases, and not the
        # intermediate table objects
        if (isinstance(parent, robot.parsing.model.TestDataDirectory) or 
            isinstance(parent, robot.parsing.model.TestCaseFile) or 
            isinstance(parent, robot.parsing.model.TestCase)):
            result.append(parent)
        parent = parent.parent
        result.reverse()
    return result


class TestCasePopup(wx.Menu):
    '''Popup menu for test case objects'''
    def __init__(self, *args, **kwargs):
        super(TestCasePopup, self).__init__(*args, **kwargs)

        self.Append(ID_SELECT_ALL, "Select All")
        self.Append(ID_DESELECT_ALL, "Deselect All")
        self.AppendSeparator()
        self.Append(ID_NEW_TESTCASE, "New Test Case")
        self.Append(ID_DELETE_ITEM, "Delete")
        self.AppendSeparator()
        self.Append(ID_RSA, "RSA")

    def update(self, tree):
        '''Update the menu prior to it being displayed

        This can be modified to enable or disable items based
        on the current environment
        '''
        pass

        
class TestCaseFilePopup(wx.Menu):
    '''Popup menu for test suite file objects'''
    def __init__(self, *args, **kwargs):
        super(TestCaseFilePopup, self).__init__(*args, **kwargs)

        self.Append(ID_SELECT_ALL, "Select All")
        self.Append(ID_DESELECT_ALL, "Deselect All")
        self.AppendSeparator()
        self.Append(ID_SELECT_CHILDREN, "Select All Children")
        self.Append(ID_DESELECT_CHILDREN, "Deselect All Children")
        self.AppendSeparator()
        self.Append(ID_NEW_TESTCASE, "New Test Case")
        self.Append(ID_DELETE_ITEM, "Delete")
        self.AppendSeparator()
        self.Append(ID_RSA, "RSA")

    def update(self, tree):
        '''Update the menu prior to it being displayed

        This can be modified to enable or disable items based
        on the current environment
        '''
        pass

class TestSuiteDirectoryPopup(wx.Menu):
    '''Popup menu for test suite file objects'''
    def __init__(self, *args, **kwargs):
        super(TestSuiteDirectoryPopup, self).__init__(*args, **kwargs)

        self.Append(ID_SELECT_ALL, "Select All")
        self.Append(ID_DESELECT_ALL, "Deselect All")
        self.AppendSeparator()
        self.Append(ID_SELECT_CHILDREN, "Select All Children")
        self.Append(ID_DESELECT_CHILDREN, "Deselect All Children")
        self.AppendSeparator()
        self.Append(ID_NEW_SUITE, "New Test Suite")
        self.Append(ID_DELETE_ITEM, "Delete")
        self.AppendSeparator()
        self.Append(ID_RSA, "RSA")

    def update(self, tree):
        '''Update the menu prior to it being displayed

        This can be modified to enable or disable items based
        on the current environment
        '''
        pass

        
GreenBullet = PyEmbeddedImage(
    "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAM1J"
    "REFUOI3tkr0OAVEQRr8VEQkJ1RLRivAKKg+xhYjWa/AcW1Ao9xGUSiIRCpFIVCob4u/ae3fu"
    "6GUKyXZiysn5TmYm4zAzklQqUfo3BGmp2Z62KgD6TOwhZlhtA46sP+vMj18JAPS71d6gnm8i"
    "ogi7cDuYrMYAMPwExRU4Zq+Wa2B92WAZLlHIFsGKPImVBdriYe546gdiaxDFL9CTxFFFASkK"
    "Duc9ytkS3IyL2/UKc9KBxIo3sIr8yWIEUuSRIphQB3Q2vsQ6/1dOLngDxfpfkNVlZ28AAAAA"
    "SUVORK5CYII=")
getGreenBulletBitmap = GreenBullet.GetBitmap

RedBullet = PyEmbeddedImage(
    "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAMdJ"
    "REFUOI3dkj0KwlAQhGeTPE8gCCGIxxA8R67gIYTA8xpeIZWFhaBY2VgJEgRrDdhIfkzUxJf1"
    "AL4iEhBxYLvZj91hiJnRREaj7X8GtMhGi+RTUJBbFFxMkkciW+tl5vcRkGosuVzMuZhNOfI8"
    "3oOkzqu9oGC41aAPtVmjXC0hOm2kMNzaL9wqQEUxVJyA7w9U1wwJzPoZZICfb3eA0wM7XcRR"
    "ihOEr/OSrokHMu0MGGYw3RSEEMI/w5qMOAprAT7Rrxbpm4AXDVdrGGTBi2sAAAAASUVORK5C"
    "YII=")
getRedBulletBitmap = RedBullet.GetBitmap

BlueBullet = PyEmbeddedImage(
    "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAL5J"
    "REFUOI3tkjsKwlAQRU9EQRDUwkpE3IC9LiRbEHcRQVeRBdhkDXaKjZ1bUEHEIj81eT97mUJI"
    "J045nDlwL+M556gytUrXvyGoS0tvTR+YYbWPKUG/ItQjdPPB5SsBMFuMbTDtwks3ONxUsNxn"
    "AItPUI5gSn/SsezOhs1J0Ws2oEx9CZUF+klcGhJlKKwj1xaKRERlQZFHx/uDURuGLUeSxZCe"
    "IwmVO1BZuNpeQeU+RQrpJSK/hhLq/V+5uuANlCtSkjpF1zcAAAAASUVORK5CYII=")
getBlueBulletBitmap = BlueBullet.GetBitmap

WhiteBullet = PyEmbeddedImage(
    "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAJpJ"
    "REFUOI3tkrEJw0AMRZ9DdJ0XCHisW8FrJHN4hdvL9bWyOf1USRHOEHAXokqIpwcSf5DEmbqc"
    "2v4NwbU3XNf1BsxAloSkImmZpmn9SgDM4zjezQyAbdvutVaAxyd4dEI2M/Z9x90xMyTlHtgV"
    "vLIREe/+KC9HguLupJRIKeHutNZKj+3+ICKWWiuSsiRaayUilh47/KN8XvAE8EhTts2QCn0A"
    "AAAASUVORK5CYII=")
getWhiteBulletBitmap = WhiteBullet.GetBitmap


